﻿using System;
using System.Reflection;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using Microsoft.CSharp;

namespace QuantumFractals
{
    public static class Evaluator
    {
        private static int formulaCounter = 0;

        public static MethodInfo AddFormula(string formula)
        {
            return AddFormula(formula, "double", new Dictionary<string, string>());
        }

        public static MethodInfo AddFormula(string formula, string type)
        {
            return AddFormula(formula, type, new Dictionary<string, string>());
        }

        public static MethodInfo AddFormula(string formula, Dictionary<string, string> constants)
        {
            return AddFormula(formula, "double", constants);
        }

        public static MethodInfo AddFormula(string formula, string type,
            Dictionary<string, string> constants)
        {
            return AddFormula(formula, type, constants, new Dictionary<string, string>());
        }

        public static MethodInfo AddFormula(string formula, string type,
            Dictionary<string, string> constants, Dictionary<string, string> parameters)
        {
            if (formula == null)
                throw new Exception("Formula is empty");
            string className = String.Format("FractalFormula{0}", formulaCounter);
            formulaCounter++;
            string methodParameters = "";
            string methodConstants = "";
            foreach (string name in parameters.Keys)
               methodParameters += String.Format("{1} {0},",
                  name, ReplaceMathFunctions(parameters[name]));
            if (methodParameters.Length > 0)
                methodParameters = methodParameters.Remove(methodParameters.Length - 1);
            foreach (string name in constants.Keys)
                methodConstants += String.Format("      double {0} = {1};\r\n",
                    name, ReplaceMathFunctions(constants[name]));
            string code = string.Format(
                "using System;\r\n" +
                "public static class {0}\r\n" +
                "{{\r\n" +
                "   public static {3} Calculate({4})\r\n" +
                "   {{\r\n" +
                "{2}\r\n" +
                "      return ({3})({1});\r\n" +
                "   }}\r\n" +
                "}}\r\n", className, ReplaceMathFunctions(formula), methodConstants, type, methodParameters);
            CompilerResults compilerResults = CompileScript(code);
            if (compilerResults.Errors.HasErrors)
                throw new Exception(String.Format(
                    "Type {0} formula {1} has a syntax error.", type, formula));

            Assembly assembly = compilerResults.CompiledAssembly;
            MethodInfo method = assembly.GetType(className).GetMethod("Calculate");
            return method;
        }

        public static string ReplaceMathFunctions(string formula)
        {
            if (formula != null)
            {
                formula = formula.Replace("abs(", "Math.Abs(");
                formula = formula.Replace("acos(", "Math.Acos(");
                formula = formula.Replace("asin(", "Math.Asin(");
                formula = formula.Replace("atan(", "Math.Atan(");
                formula = formula.Replace("atan2(", "Math.Atan2(");
                formula = formula.Replace("ceiling(", "Math.Ceiling(");
                formula = formula.Replace("cos(", "Math.Cos(");
                formula = formula.Replace("cosh(", "Math.Cosh(");
                formula = formula.Replace("exp(", "Math.Exp(");
                formula = formula.Replace("floor(", "Math.Floor(");
                formula = formula.Replace("ieeeremainder(", "Math.IEEERemainder(");
                formula = formula.Replace("log(", "Math.Log(");
                formula = formula.Replace("log10(", "Math.Log10(");
                formula = formula.Replace("max(", "Math.Max(");
                formula = formula.Replace("min(", "Math.Min(");
                formula = formula.Replace("pow(", "Math.Pow(");
                formula = formula.Replace("round(", "Math.Round(");
                formula = formula.Replace("sign(", "Math.Sign(");
                formula = formula.Replace("sin(", "Math.Sin(");
                formula = formula.Replace("sinh(", "Math.Sinh(");
                formula = formula.Replace("sqrt(", "Math.Sqrt(");
                formula = formula.Replace("tan(", "Math.Tan(");
                formula = formula.Replace("tanh(", "Math.Tanh(");
                formula = formula.Replace("truncate(", "Math.Truncate(");
                formula = formula.Replace("PI", "Math.PI");
                formula = formula.Replace("E", "Math.E");
                formula = formula.Replace("PHI", "1.618033988");
            }
            return formula;
        }

        public static double CalculateFormula(string formula, Dictionary<string, string> constants)
        {
            return (double)AddFormula(formula, "double", constants).Invoke(null, null);
        }

        public static double CalculateFormula(string formula)
        {
            return CalculateFormula(formula, new Dictionary<string,string>());
        }

        public static CompilerResults CompileScript(string source)
        {
            CompilerParameters parms = new CompilerParameters();

            parms.GenerateExecutable = false;
            parms.GenerateInMemory = true;
            parms.IncludeDebugInformation = true;
            parms.WarningLevel = 3;

            CodeDomProvider compiler = CSharpCodeProvider.CreateProvider("CSharp");

            return compiler.CompileAssemblyFromSource(parms, source);
        }

        public static QPoint[] CalculateFormulaPoints(string[] formulaPoints)
        {
            return CalculateFormulaPoints(formulaPoints, new Dictionary<string, string>());
        }

        public static QPoint[] CalculateFormulaPoints(string[] formulaPoints, Dictionary<string, string> constants)
        {
            string className = String.Format("FractalFormula{0}", formulaCounter);
            formulaCounter++;
            if (formulaPoints.Length < 1)
                throw new Exception("To small lines in points");

            string points = "{ ";
            for (int i = 0; i < formulaPoints.Length - 1; i++)
                points += "{" + ReplaceMathFunctions(formulaPoints[i]) + "}, ";
            points += "{" + ReplaceMathFunctions(formulaPoints[formulaPoints.Length - 1]) + "} }";

            string methodConstants = "";
            foreach (string name in constants.Keys)
                methodConstants += String.Format("      double {0} = {1};\r\n",
                    name, ReplaceMathFunctions(constants[name]));

            string code = string.Format(
                "using System;\r\n" +
                //"{{\r\n" +
                "   public static class {0}\r\n" +
                "   {{\r\n" +
                "      public static double[,] GetFractalPoints()\r\n" +
                "      {{\r\n" +
                "         {3}\r\n" +
                "         return new double[{2}, 3] {1};\r\n" +
                "      }}\r\n" +
                //"   }}\r\n" +
                "}}", className, points, formulaPoints.Length, methodConstants);
            CompilerResults compilerResults = CompileScript(code);
            if (compilerResults.Errors.HasErrors)
                throw new Exception(String.Format("Type double[,] formula has a syntax error: {0}.", 
                    compilerResults.Errors[0].ErrorText));

            Assembly assembly = compilerResults.CompiledAssembly;
            MethodInfo method = assembly.GetType(className).GetMethod("GetFractalPoints");
            double[,] table = (double[,])method.Invoke(null, null);
            QPoint[] result = new QPoint[formulaPoints.Length];
            for (int i = 0; i < formulaPoints.Length; i++)
            {
                result[i].X = table[i, 0];
                result[i].Y = table[i, 1];
                result[i].Z = table[i, 2];
            }
            return result;
        }
    }
}
